const assert = require("assert");

const addon = require("..");

describe("neon::export macro", () => {
  describe("globals", globals);
  describe("functions", functions);
  describe("classes", classes);
});

function globals() {
  it("values", () => {
    assert.strictEqual(addon.NUMBER, 42);
    assert.strictEqual(addon.STRING, "Hello, World!");
    assert.strictEqual(addon.renamedString, "Hello, World!");
  });

  it("json", () => {
    assert.deepStrictEqual(addon.MESSAGES, ["hello", "neon"]);
    assert.deepStrictEqual(addon.renamedMessages, ["hello", "neon"]);
  });

  it("LazyLock", () => {
    assert.strictEqual(addon.LAZY_LOCK_HELLO, "Hello, Neon!");
  });
}

function functions() {
  it("void function", () => {
    assert.strictEqual(addon.noArgsOrReturn(), undefined);
  });

  it("add - sync", () => {
    assert.strictEqual(addon.simpleAdd(1, 2), 3);
    assert.strictEqual(addon.renamedAdd(1, 2), 3);
  });

  it("add - task", async () => {
    const p1 = addon.addTask(1, 2);
    const p2 = addon.renamedAddTask(1, 2);

    assert.ok(p1 instanceof Promise);
    assert.ok(p2 instanceof Promise);

    assert.strictEqual(await p1, 3);
    assert.strictEqual(await p2, 3);
  });

  it("json sort", () => {
    const arr = ["b", "c", "a"];
    const expected = [...arr].sort();

    assert.deepStrictEqual(addon.jsonSort(arr), expected);
    assert.deepStrictEqual(addon.renamedJsonSort(arr), expected);
  });

  it("json sort - task", async () => {
    const arr = ["b", "c", "a"];
    const expected = [...arr].sort();
    const p1 = addon.jsonSortTask(arr);
    const p2 = addon.renamedJsonSortTask(arr);

    assert.ok(p1 instanceof Promise);
    assert.ok(p2 instanceof Promise);

    assert.deepStrictEqual(await p1, expected);
    assert.deepStrictEqual(await p2, expected);
  });

  it("can use context and handles", () => {
    const actual = addon.concatWithCxAndHandle("Hello,", " World!");
    const expected = "Hello, World!";

    assert.strictEqual(actual, expected);
  });

  it("error conversion", () => {
    const msg = "Oh, no!";
    const expected = new Error(msg);

    assert.throws(() => addon.failWithThrow(msg), expected);
  });

  it("tasks are concurrent", async () => {
    const time = 500;
    const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
    const start = process.hrtime.bigint();

    await Promise.all([addon.sleepTask(time), sleep(time)]);

    const end = process.hrtime.bigint();
    const duration = end - start;

    // If `addon.sleepTask` blocks the thread, the tasks will run sequentially
    // and take a minimum of 2x `time`. Since they are run concurrently, we
    // expect the time to be closer to 1x `time`.
    const maxExpected = 2000000n * BigInt(time);

    assert.ok(duration < maxExpected);
  });

  it("can use generic Cx in exported functions", () => {
    assert.strictEqual(addon.numberWithCx(42), 42);
  });

  it("i32 parameters", () => {
    assert.strictEqual(addon.addI32(5, 3), 8);
    assert.strictEqual(addon.addI32(-10, 20), 10);
    assert.strictEqual(addon.addI32(-5, -3), -8);
    assert.strictEqual(addon.toI32(Infinity), 0);
    assert.strictEqual(addon.toI32(-Infinity), 0);
  });

  it("u32 parameters", () => {
    assert.strictEqual(addon.addU32(5, 3), 8);
    assert.strictEqual(addon.addU32(100, 200), 300);
    assert.strictEqual(addon.addU32(0, 42), 42);
    assert.strictEqual(addon.toU32(Infinity), 0);
    assert.strictEqual(addon.toU32(-Infinity), 0);
  });
}

function classes() {
  it("can create and use exported class", () => {
    const ExportedPoint = addon.ExportedPoint;

    // Test that the class was exported
    assert.strictEqual(typeof ExportedPoint, "function");

    // Test const properties
    assert.strictEqual(ExportedPoint.ORIGIN_X, 0.0);
    assert.strictEqual(ExportedPoint.ORIGIN_Y, 0.0);

    // Test class instantiation and methods
    const point1 = new ExportedPoint(3, 4);
    const point2 = new ExportedPoint(0, 0);

    assert.strictEqual(point1.x(), 3);
    assert.strictEqual(point1.y(), 4);
    assert.strictEqual(point2.x(), 0);
    assert.strictEqual(point2.y(), 0);

    // Test distance calculation
    assert.strictEqual(point1.distance(point2), 5); // 3-4-5 triangle
  });

  it('can use custom export name - CASE 1: class, name = "..."', () => {
    const RenamedClass = addon.RenamedClass;

    // Test that the class was exported with custom name
    assert.strictEqual(typeof RenamedClass, "function");

    // Test that the original name is not exported
    assert.strictEqual(addon.CustomNamedClass, undefined);

    // Test class functionality
    const instance = new RenamedClass("test value");
    assert.strictEqual(instance.getValue(), "test value");

    // Test that the class name itself is RenamedClass
    assert.strictEqual(RenamedClass.name, "RenamedClass");
  });

  it('CASE 2: class(name = "...") - parenthesized syntax', () => {
    const ParenRenamedClass = addon.ParenRenamedClass;

    // Test that the class was exported with the name from class(name = "...")
    assert.strictEqual(typeof ParenRenamedClass, "function");

    // Test that the original Rust name is not exported
    assert.strictEqual(addon.Case2Class, undefined);

    // Test class functionality
    const instance = new ParenRenamedClass("hello");
    assert.strictEqual(instance.getMessage(), "hello");

    // Test that the JavaScript class name is ParenRenamedClass
    assert.strictEqual(ParenRenamedClass.name, "ParenRenamedClass");
  });

  it('CASE 3: class(name = "Internal"), name = "External" - separate names', () => {
    const ExternalExportName = addon.ExternalExportName;

    // Test that the class was exported with the outer name
    assert.strictEqual(typeof ExternalExportName, "function");

    // Test that the Rust name is not exported
    assert.strictEqual(addon.Case3Class, undefined);

    // Test that the inner class name is not directly exported
    assert.strictEqual(addon.InternalClassName, undefined);

    // Test class functionality
    const instance = new ExternalExportName(42);
    assert.strictEqual(instance.getData(), 42);

    // Test that the JavaScript class name is the inner name (InternalClassName)
    // but the export binding is the outer name (ExternalExportName)
    assert.strictEqual(ExternalExportName.name, "InternalClassName");
  });

  it("can export async fn with JSON", async () => {
    const input = [1, 2, 3, 4, 5];
    const result = await addon.exportAsyncJsonTest(input);

    // Should multiply each element by 3
    assert.deepStrictEqual(result, [3, 6, 9, 12, 15]);
  });
}
